﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using System.Xml;
using System.Globalization;
using System.Diagnostics;

namespace fleXdoc.Api
{
    internal class XsltExtensions
    {

        private const string TRACE_PREFIX = "fleXdoc.Api.XsltExtensions: ";

        private static TraceSource _traceSrc = new TraceSource("fleXdoc.Api", SourceLevels.Off); // When not in config (or commented out), don't (attempt to) trace anything

        private CultureInfo _renderCulture = null;

        public XsltExtensions(CultureInfo renderCulture)
        {
            _renderCulture = renderCulture;
        }

        /// <summary>
        /// Dynamically selects nodes based on the xpath-expression
        /// </summary>
        /// <param name="context">The context node to execute the expression on</param>
        /// <param name="expression">The expression itself (can use only the namespace-prefix specified by nsPrefix or no prefix at all)</param>
        /// <param name="nsPrefix">The namespace prefix which maps to the namespace as specified by namespaceURI</param>
        /// <param name="nsURI">The namespace which is is mapped to the prefix specified by nsPrefix</param>
        /// <param name="sourceName">The source (custom XML element) that invokes this method, eg: 'UseContext'.</param>
        /// <returns>The resulting node(set) or null when no context</returns>
        public XPathNodeIterator Select(XPathNodeIterator context, string expression, string nsPrefix, string nsURI, string sourceName)
        {
            try
            {
                XPathNodeIterator retVal = context;

                // The context (iterator) is passed from xslt and therefore not yet moved to the first item
                if (context.MoveNext())
                {
                    XPathExpression xpathExpr = context.Current.Compile(expression);

                    NameTable nt = context.Current.NameTable as NameTable;
                    if (nt != null) // Useful nametable found, so we can use fleXdocXsltContext and its advanced functions
                    {
                        fleXdocXsltContext xsltCtx = new fleXdocXsltContext(nt);
                        if (!xsltCtx.HasNamespace(nsPrefix))
                        {
                            xsltCtx.AddNamespace(nsPrefix, nsURI);
                        }
                        xsltCtx.CurrentNode = context.Current.Select("."); // Convert current node to a nodeset
                        xpathExpr.SetContext(xsltCtx);
                    }
                    else // No useful nametable found, so we must use the regular XsltContext (and a namespace manager)
                    {
                        XmlNamespaceManager nsMgr = new XmlNamespaceManager(context.Current.NameTable);
                        if (!nsMgr.HasNamespace(nsPrefix))
                        {
                            nsMgr.AddNamespace(nsPrefix, nsURI);
                        }
                        xpathExpr.SetContext(nsMgr);
                    }

                    retVal = context.Current.Select(xpathExpr);
                }

                if (_traceSrc.Switch.ShouldTrace(TraceEventType.Verbose))
                {
                    string msg = String.Format(CultureInfo.InvariantCulture, "{0}: Expression '{1}' resulted in a nodeset of {2} node(s).", sourceName, expression, (retVal != null ? retVal.Count : 0));
                    _traceSrc.TraceEvent(TraceEventType.Verbose, 0, TRACE_PREFIX + msg);
                }

                return retVal;
            }
            catch (Exception ex)
            {
                string msg = String.Format(CultureInfo.InvariantCulture, "{0}: Error selecting a nodeset using xpath: '{1}'. Details: {2}", sourceName, expression, ex);
                _traceSrc.TraceEvent(TraceEventType.Error, 0, msg);
                throw new Exception(msg);
            }
        }

        /// <summary>
        /// Dynamically evaluates the xpath-expression
        /// </summary>
        /// <param name="context">The context node to execute the expression on</param>
        /// <param name="expression">The expression itself (can use only the namespace-prefix specified by nsPrefix or no prefix at all)</param>
        /// <param name="nsPrefix">The namespace prefix which maps to the namespace as specified by namespaceURI</param>
        /// <param name="nsURI">The namespace which is is mapped to the prefix specified by nsPrefix</param>
        /// <returns>The resulting value of the node(set) or null when no context or no value</returns>
        public object Evaluate(XPathNodeIterator context, string expression, string nsPrefix, string nsURI, string sourceName)
        {
            try
            {
                object retValue = context;

                // The context (iterator) is passed from xslt and therefore not yet moved to the first item
                if (context.MoveNext())
                {
                    XPathExpression xpathExpr = context.Current.Compile(expression);

                    NameTable nt = context.Current.NameTable as NameTable;
                    if (nt != null) // Useful nametable found, so we can use fleXdocXsltContext and its advanced functions
                    {
                        fleXdocXsltContext xsltCtx = new fleXdocXsltContext(nt);
                        if (!xsltCtx.HasNamespace(nsPrefix))
                        {
                            xsltCtx.AddNamespace(nsPrefix, nsURI);
                        }
                        xsltCtx.CurrentNode = context.Current.Select("."); // Convert current node to a nodeset
                        xpathExpr.SetContext(xsltCtx);
                    }
                    else // No useful nametable found, so we must use the regular XsltContext (and a namespace manager)
                    {
                        XmlNamespaceManager nsMgr = new XmlNamespaceManager(context.Current.NameTable);
                        if (!nsMgr.HasNamespace(nsPrefix))
                        {
                            nsMgr.AddNamespace(nsPrefix, nsURI);
                        }
                        xpathExpr.SetContext(nsMgr);
                    }
                    
                    retValue = context.Current.Evaluate(xpathExpr);
                }

                if (_traceSrc.Switch.ShouldTrace(TraceEventType.Verbose))
                {
                    XPathNodeIterator retValAsNodeIterator = retValue as XPathNodeIterator;
                    string valueToTrace = null;
                    if (retValAsNodeIterator != null)
                    {
                        retValAsNodeIterator = retValAsNodeIterator.Clone(); // We don't want to navigate on the actual results
                        if (retValAsNodeIterator.MoveNext())
                        {
                            valueToTrace = retValAsNodeIterator.Current.Value;
                        }
                    }
                    string msg = String.Format(CultureInfo.InvariantCulture, "{0}: Expression '{1}' evaluated to '{2}'", sourceName, expression,
                        valueToTrace ?? retValue);
                    _traceSrc.TraceEvent(TraceEventType.Verbose, 0, msg);
                }
                
                return retValue;
            }
            catch (Exception ex)
            {
                string msg = String.Format(CultureInfo.InvariantCulture, "{0}: Error evaluating a nodeset using xpath: '{1}'. Details: {2}", sourceName, expression, ex);
                _traceSrc.TraceEvent(TraceEventType.Error, 0, msg); // Checks ShouldTrace itself as well
                throw new Exception(msg);
            }
        }

        public string FormatNode(XPathNodeIterator context, string formatType, string formatString)
        {
            // The iterator is passed from xslt and therefore not yet moved to the first item
            if (context.MoveNext())
            {
                return FormatString(context.Current.Value, formatType, formatString);
            }
            else
            {
                return FormatString(string.Empty, formatType, formatString);
            }
        }

        public string FormatString(string dataItem, string formatType, string formatString)
        {
            if (String.IsNullOrEmpty(dataItem))
            {
                return string.Empty;
            }
            else if (String.IsNullOrEmpty(formatType) || (formatType == "Text"))
            {
                return dataItem;
            }
            else
            {
                string customFormattingString = string.Empty;

                if (formatType.StartsWith("Numeric"))
                {
                    customFormattingString = "N0";
                    int decimals = 0;
                    if (formatType.Length > 7) // decimals specified
                    {
                        string postFix = formatType.Substring(7);
                        if (postFix == "Custom")
                        {
                            customFormattingString = formatString;
                        }
                        else
                        {
                            if (int.TryParse(formatType.Substring(7), out decimals))
                            {
                                customFormattingString = "N" + decimals.ToString();
                            }
                        }
                    }

                    decimal numericItem;
                    if (decimal.TryParse(dataItem, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out numericItem))
                    {
                        return numericItem.ToString(customFormattingString, _renderCulture);
                    }
                }
                else if (formatType.StartsWith("DateTime"))
                {
                    if (formatType.Length > 8)
                    {
                        string postFix = formatType.Substring(8);
                        if (postFix == "Short")
                        {
                            customFormattingString = "d";
                        }
                        else if (postFix == "Long")
                        {
                            customFormattingString = "D";
                        }
                        else if (postFix == "Custom")
                        {
                            customFormattingString = formatString;
                        }
                    }

                    DateTime dateTime;
                    if (DateTime.TryParse(dataItem, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out dateTime))
                    {
                        return dateTime.ToString(customFormattingString, _renderCulture);
                    }
                }

                return "?(unsupported formatType)";

            }
        }
    }
}
